﻿/*
Copyright (c) 2008 William Duff

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using LiveUpload.Facebook.Properties;
using System.Globalization;

namespace LiveUpload.Facebook
{
    static public class FacebookParser
    {
        #region Private Fields

        /// <summary>
        /// A DateTime representation of the Unix Epoch. Used for Unix timestamp conversions.
        /// </summary>
        private static DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);

        #endregion

        #region Private Methods

        /// <summary>
        /// Using the XmlNamespaceManager is a workaround to get XPath queries to work against 
        /// the response XML from Facebook. Because Facebook redefines a default namespace for 
        /// their XML (xmlns="http://api.facebook.com/1.0/"), I define the "fb" namespace with the 
        /// same namespace URI and query all elements using the fb namespace.
        /// </summary>
        /// <param name="documentElement">
        /// An element of the XmlDocument to create a namespace manager for.
        /// </param>
        /// <returns>
        /// An XmlNamespaceManager with the "fb" namespace defined.
        /// </returns>
        private static XmlNamespaceManager GetFacebookNamespaceManager(XmlElement element)
        {
            XmlNamespaceManager nsmgr = 
                new XmlNamespaceManager(element.OwnerDocument.NameTable);
            nsmgr.AddNamespace("fb", element.OwnerDocument.DocumentElement.NamespaceURI);
            return nsmgr;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Checks to see if the XML returned from Facebook is a valid response.
        /// </summary>
        /// <param name="responseXml">
        /// The XML document returned from Faceboook.
        /// </param>
        /// <returns>
        /// true if the response is valid and false otherwise.
        /// </returns>
        public static bool isResponseValid(XmlDocument responseXml)
        {
            // An XML response from Facebook that indicates an error occurred will have a root
            // element <error_response>.
            return responseXml.DocumentElement.LocalName != "error_response";
        }

        /// <summary>
        /// If the XML returned from Facebook is an error, this method is used to extract the error
        /// message from the XML.
        /// </summary>
        /// <param name="responseXml">
        /// The XML document returned as an error from Facebook.
        /// </param>
        /// <returns>
        /// A message describing the error returned by Facebook.
        /// </returns>
        public static string GetResponseError(XmlDocument responseXml)
        {            
            XmlNamespaceManager nsmgr = GetFacebookNamespaceManager(responseXml.DocumentElement);
            XmlElement errorMessageElement =
                responseXml.DocumentElement.SelectSingleNode("fb:error_msg", nsmgr) as XmlElement;

            if (errorMessageElement != null)
            {
                return errorMessageElement.InnerText;
            }
            else
            {
                // If we couldn't get the <error_msg> element, return a generic error.
                return Resources.FacebookGenericError;
            }
        }

        /// <summary>
        /// Converts a Unix timestamp into a nullable DateTime object. If the Unix timestamp is 
        /// zero, this function will return null.
        /// </summary>
        /// <param name="timestamp">
        /// A Unix timestamp.
        /// </param>
        /// <returns>
        /// A nullable DateTime object representing the same time as the Unix timestamp.
        /// </returns>
        public static DateTime? ConvertFromUnixTimestamp(double timestamp)
        {
            DateTime? time = null;

            if (timestamp > 0)
            {
                time = UnixEpoch.AddSeconds(timestamp);
            }

            return time;
        }

        /// <summary>
        /// Converts a nullable DateTime object into a Unix timestamp. If the DateTime object is 
        /// null, this function will return zero.
        /// </summary>
        /// <param name="time">
        /// A nullable DateTime object.
        /// </param>
        /// <returns>
        /// A Unix timestamp representing the same time as the nullable DateTime object.
        /// </returns>
        public static double ConvertToUnixTimestamp(DateTime? time)
        {
            if (time.HasValue)
            {
                TimeSpan sinceUnixEpoch = time.Value.Subtract(UnixEpoch);
                return Math.Floor(sinceUnixEpoch.TotalSeconds);
            }
            else
            {
                return 0d;
            }
        }

        /// <summary>
        /// Deserializes an XmlElement into an authentication token.
        /// </summary>
        /// <param name="tokenElement">
        /// A XmlElement representing an authentication token.
        /// </param>
        /// <returns>
        /// An authentication token.
        /// </returns>
        public static string DeserializeAuthToken(XmlElement tokenElement)
        {
            if (tokenElement == null)
            {
                throw new ArgumentNullException("tokenElement");
            }

            return tokenElement.InnerText;
        }

        /// <summary>
        /// Serializes an AccountSession object into a XmlElement.
        /// </summary>
        /// <param name="accountSession">
        /// An AccountSession object.
        /// </param>
        /// <param name="document">
        /// A XmlDocument to serialize the AccountSession object into.
        /// </param>
        /// <returns>
        /// A XmlElement representing an AccountSession.
        /// </returns>
        public static XmlElement SerializeAccountSession(AccountSession accountSession, 
            XmlDocument document)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }
            if (accountSession == null)
            {
                throw new ArgumentNullException("accountSession");
            }

            XmlElement sessionElement = document.CreateElement("session");

            XmlElement sessionKeyElement = document.CreateElement("session_key");
            sessionKeyElement.InnerText = accountSession.Key;
            sessionElement.AppendChild(sessionKeyElement);

            XmlElement sessionSecretElement = document.CreateElement("secret");
            sessionSecretElement.InnerText = accountSession.Secret;
            sessionElement.AppendChild(sessionSecretElement);

            XmlElement ownerIdElement = document.CreateElement("uid");
            ownerIdElement.InnerText = accountSession.OwnerId;
            sessionElement.AppendChild(ownerIdElement);

            XmlElement expiresElement = document.CreateElement("expires");
            expiresElement.InnerText = accountSession.ExpiresUnixTimestamp.ToString();
            sessionElement.AppendChild(expiresElement);

            return sessionElement;
        }

        /// <summary>
        /// Deserializes a XmlElement into an AccountSession.
        /// </summary>
        /// <param name="tokenElement">
        /// A XmlElement representing an AccountSession.
        /// </param>
        /// <returns>
        /// An AccountSession.
        /// </returns>
        public static AccountSession DeserializeAccountSession(XmlElement sessionElement)
        {
            if (sessionElement == null)
            {
                throw new ArgumentNullException("sessionElement");
            }

            XmlNamespaceManager nsmgr = GetFacebookNamespaceManager(sessionElement);

            string key = sessionElement.SelectSingleNode("fb:session_key", nsmgr).InnerText;
            string secret = sessionElement.SelectSingleNode("fb:secret", nsmgr).InnerText;
            string ownerId = sessionElement.SelectSingleNode("fb:uid", nsmgr).InnerText;
            
            double expiresUnixTimestamp = 0d;
            double.TryParse(sessionElement.SelectSingleNode("fb:expires", nsmgr).InnerText, 
                NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresUnixTimestamp);

            return new AccountSession(key, secret, ownerId, expiresUnixTimestamp);
        }

        /// <summary>
        /// Serializes an Account object into a XmlElement.
        /// </summary>
        /// <param name="accountSession">
        /// An Account object.
        /// </param>
        /// <param name="document">
        /// A XmlDocument to serialize the Account object into.
        /// </param>
        /// <returns>
        /// A XmlElement representing an Account.
        /// </returns>
        public static XmlElement SerializeAccount(Account user, XmlDocument document)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }
            if (user == null)
            {
                throw new ArgumentNullException("user");
            }

            XmlElement userElement = document.CreateElement("user");
            userElement.SetAttribute("id", user.Id);

            XmlElement sessionElement = SerializeAccountSession(user.Session, document);
            userElement.AppendChild(sessionElement);

            return userElement;
        }

        /// <summary>
        /// Deserializes a XmlElement into an Account.
        /// </summary>
        /// <param name="tokenElement">
        /// A XmlElement representing an Account.
        /// </param>
        /// <returns>
        /// An Account.
        /// </returns>
        public static Account DeserializeAccount(XmlElement userElement)
        {
            if (userElement == null)
            {
                throw new ArgumentNullException("userElement");
            }

            XmlNamespaceManager nsmgr = GetFacebookNamespaceManager(userElement);

            string id = userElement.GetAttribute("id");

            XmlElement sessionElement = 
                userElement.SelectSingleNode("fb:session", nsmgr) as XmlElement;
            AccountSession session = DeserializeAccountSession(sessionElement);

            return new Account(id, session);
        }

        /// <summary>
        /// Serializes an Album object into a XmlElement.
        /// </summary>
        /// <param name="accountSession">
        /// An Album object.
        /// </param>
        /// <param name="document">
        /// A XmlDocument to serialize the Album object into.
        /// </param>
        /// <returns>
        /// A XmlElement representing an Album.
        /// </returns>
        public static XmlElement SerializeAlbum(Album album, XmlDocument document)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }
            if (album == null)
            {
                throw new ArgumentNullException("album");
            }

            XmlElement albumElement = document.CreateElement("album");

            XmlElement idElement = document.CreateElement("aid");
            idElement.InnerText = album.Id;
            albumElement.AppendChild(idElement);

            XmlElement coverPhotoIdElement = document.CreateElement("cover_pid");
            coverPhotoIdElement.InnerText = album.CoverPhotoId;
            albumElement.AppendChild(coverPhotoIdElement);

            XmlElement ownerIdElement = document.CreateElement("owner");
            ownerIdElement.InnerText = album.OwnerId;
            albumElement.AppendChild(ownerIdElement);

            XmlElement nameElement = document.CreateElement("name");
            nameElement.InnerText = album.Name;
            albumElement.AppendChild(nameElement);

            XmlElement createdElement = document.CreateElement("created");
            createdElement.InnerText = album.CreatedUnixTimestamp.ToString();
            albumElement.AppendChild(createdElement);

            XmlElement modifiedElement = document.CreateElement("modified");
            modifiedElement.InnerText = album.ModifiedUnixTimestamp.ToString();
            albumElement.AppendChild(modifiedElement);

            XmlElement descriptionElement = document.CreateElement("description");
            descriptionElement.InnerText = album.Description;
            albumElement.AppendChild(descriptionElement);

            XmlElement locationElement = document.CreateElement("location");
            locationElement.InnerText = album.Location;
            albumElement.AppendChild(locationElement);

            XmlElement linkElement = document.CreateElement("link");
            linkElement.InnerText = album.Link;
            albumElement.AppendChild(linkElement);

            XmlElement sizeElement = document.CreateElement("size");
            sizeElement.InnerText = album.Size.ToString();
            albumElement.AppendChild(sizeElement);

            return albumElement;
        }

        /// <summary>
        /// Deserializes a XmlElement into an Album.
        /// </summary>
        /// <param name="tokenElement">
        /// A XmlElement representing an Album.
        /// </param>
        /// <returns>
        /// An Album.
        /// </returns>
        public static Album DeserializeAlbum(XmlElement albumElement)
        {
            if (albumElement == null)
            {
                throw new ArgumentNullException("albumElement");
            }

            XmlNamespaceManager nsmgr = GetFacebookNamespaceManager(albumElement);

            string id = albumElement.SelectSingleNode("fb:aid", nsmgr).InnerText;
            string coverId = albumElement.SelectSingleNode("fb:cover_pid", nsmgr).InnerText;
            string ownerId = albumElement.SelectSingleNode("fb:owner", nsmgr).InnerText;
            string name = albumElement.SelectSingleNode("fb:name", nsmgr).InnerText;

            double createdUnixTimestamp = 0d;
            double.TryParse(albumElement.SelectSingleNode("fb:created", nsmgr).InnerText, 
                NumberStyles.Integer, CultureInfo.InvariantCulture, out createdUnixTimestamp);

            double modifiedUnixTimestamp = 0d;
            double.TryParse(albumElement.SelectSingleNode("fb:modified", nsmgr).InnerText, 
                NumberStyles.Float, CultureInfo.InvariantCulture, out modifiedUnixTimestamp);

            string description = albumElement.SelectSingleNode("fb:description", nsmgr).InnerText;
            string location = albumElement.SelectSingleNode("fb:location", nsmgr).InnerText;
            string link = albumElement.SelectSingleNode("fb:link", nsmgr).InnerText;

            int size = 0;
            int.TryParse(albumElement.SelectSingleNode("fb:size", nsmgr).InnerText,
                 NumberStyles.Integer, CultureInfo.InvariantCulture, out size);

            return new Album(id, coverId, ownerId, name, createdUnixTimestamp, 
                modifiedUnixTimestamp, location, description, link, size, string.Empty);
        }

        /// <summary>
        /// Serializes a Photo object into a XmlElement.
        /// </summary>
        /// <param name="accountSession">
        /// A Photo object.
        /// </param>
        /// <param name="document">
        /// A XmlDocument to serialize the Photo object into.
        /// </param>
        /// <returns>
        /// A XmlElement representing a Photo.
        /// </returns>
        public static XmlElement SerializePhoto(Photo photo, XmlDocument document)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }
            if (photo == null)
            {
                throw new ArgumentNullException("photo");
            }

            XmlElement photoElement = document.CreateElement("photo");

            XmlElement idElement = document.CreateElement("pid");
            idElement.InnerText = photo.Id;
            photoElement.AppendChild(idElement);

            XmlElement albumIdElement = document.CreateElement("aid");
            albumIdElement.InnerText = photo.AlbumId;
            photoElement.AppendChild(albumIdElement);

            XmlElement ownerIdElement = document.CreateElement("owner");
            ownerIdElement.InnerText = photo.OwnerId;
            photoElement.AppendChild(ownerIdElement);

            XmlElement srcElement = document.CreateElement("src");
            srcElement.InnerText = photo.Src;
            photoElement.AppendChild(srcElement);

            XmlElement srcBigElement = document.CreateElement("src_big");
            srcBigElement.InnerText = photo.SrcBig;
            photoElement.AppendChild(srcBigElement);

            XmlElement srcSmallElement = document.CreateElement("src_small");
            srcSmallElement.InnerText = photo.SrcSmall;
            photoElement.AppendChild(srcSmallElement);

            XmlElement linkElement = document.CreateElement("link");
            linkElement.InnerText = photo.Link;
            photoElement.AppendChild(linkElement);

            XmlElement captionElement = document.CreateElement("caption");
            captionElement.InnerText = photo.Caption;
            photoElement.AppendChild(captionElement);

            XmlElement createdElement = document.CreateElement("created");
            createdElement.InnerText = photo.CreatedUnixTimestamp.ToString();
            photoElement.AppendChild(createdElement);

            return photoElement;
        }

        /// <summary>
        /// Deserializes a XmlElement into a Photo.
        /// </summary>
        /// <param name="tokenElement">
        /// A XmlElement representing a Photo.
        /// </param>
        /// <returns>
        /// A Photo.
        /// </returns>
        public static Photo DeserializePhoto(XmlElement photoElement)
        {
            if (photoElement == null)
            {
                throw new ArgumentNullException("responseXml");
            }

            XmlNamespaceManager nsmgr = GetFacebookNamespaceManager(photoElement);

            string id = photoElement.SelectSingleNode("fb:pid", nsmgr).InnerText;
            string albumId = photoElement.SelectSingleNode("fb:aid", nsmgr).InnerText;
            string ownerId = photoElement.SelectSingleNode("fb:owner", nsmgr).InnerText;
            string src = photoElement.SelectSingleNode("fb:src", nsmgr).InnerText;
            string srcBig = photoElement.SelectSingleNode("fb:src_big", nsmgr).InnerText;
            string srcSmall = photoElement.SelectSingleNode("fb:src_small", nsmgr).InnerText;
            string link = photoElement.SelectSingleNode("fb:link", nsmgr).InnerText;
            string caption = photoElement.SelectSingleNode("fb:caption", nsmgr).InnerText;
            double createdUnixTimestamp = 0d;

            XmlElement createdElement =
                photoElement.SelectSingleNode("fb:created", nsmgr) as XmlElement;
            if (createdElement != null)
            {
                double.TryParse(createdElement.InnerText, NumberStyles.Float, 
                    CultureInfo.InvariantCulture, out createdUnixTimestamp);
            }
            else
            {
                // Newly created photos will not have a <created> tag, so we fill in the details.
                createdUnixTimestamp = ConvertToUnixTimestamp(DateTime.UtcNow);
            }

            return new Photo(id, albumId, ownerId, src, srcBig, srcSmall, link, caption,
                createdUnixTimestamp);
        }

        /// <summary>
        /// Deserializes a XmlElement into a Video.
        /// </summary>
        /// <param name="tokenElement">
        /// A XmlElement representing a Video.
        /// </param>
        /// <returns>
        /// A Video.
        /// </returns>
        public static Video DeserializeVideo(XmlElement videoElement)
        {
            if (videoElement == null)
            {
                throw new ArgumentNullException("responseXml");
            }

            XmlNamespaceManager nsmgr = GetFacebookNamespaceManager(videoElement);

            string id = videoElement.SelectSingleNode("fb:vid", nsmgr).InnerText;
            string link = videoElement.SelectSingleNode("fb:link", nsmgr).InnerText;
            string title = string.Empty;
            if (videoElement.SelectSingleNode("fb:title", nsmgr) != null)
            {
                title = videoElement.SelectSingleNode("fb:title", nsmgr).InnerText;
            }
            string description = string.Empty;
            if (videoElement.SelectSingleNode("fb:description", nsmgr) != null)
            {
                description = videoElement.SelectSingleNode("fb:description", nsmgr).InnerText;
            }

            return new Video(id, link, title, description);
        }

        /// <summary>
        /// Serializes a Friend object into a XmlElement.
        /// </summary>
        /// <param name="friendObject">
        /// A friend object.
        /// </param>
        /// <param name="document">
        /// A XmlDocument to serialize the friend object into.
        /// </param>
        /// <returns>
        /// A XmlElement representing a Friend.
        /// </returns>
        public static XmlElement SerializeFriend(Friend friendObject, XmlDocument document)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }
            if (friendObject == null)
            {
                throw new ArgumentNullException("friendObject");
            }

            XmlElement friendElement = document.CreateElement("user");

            XmlElement firstNameElement = document.CreateElement("first_name");
            firstNameElement.InnerText = friendObject.FirstName;
            friendElement.AppendChild(firstNameElement);

            XmlElement lastNameElement = document.CreateElement("last_name");
            lastNameElement.InnerText = friendObject.LastName;
            friendElement.AppendChild(lastNameElement);

            XmlElement uidElement = document.CreateElement("uid");
            uidElement.InnerText = friendObject.UID;
            friendElement.AppendChild(uidElement);

            XmlElement squarePictureUrlElement = document.CreateElement("pic_square");
            squarePictureUrlElement.InnerText = friendObject.SquarePictureUrl;
            friendElement.AppendChild(squarePictureUrlElement);

            return friendElement;
        }

        /// <summary>
        /// Deserializes a XmlElement into a Friend.
        /// </summary>
        /// <param name="friendElement">
        /// A XmlElement representing a friend.
        /// </param>
        /// <returns>
        /// A Friend.
        /// </returns>
        public static Friend DeserializeFriend(XmlElement friendElement)
        {
            if (friendElement == null)
            {
                throw new ArgumentNullException("friendElement");
            }

            XmlNamespaceManager nsmgr = GetFacebookNamespaceManager(friendElement);

            string firstName = friendElement.SelectSingleNode("fb:first_name", nsmgr).InnerText;
            string lastName = friendElement.SelectSingleNode("fb:last_name", nsmgr).InnerText;
            string uid = friendElement.SelectSingleNode("fb:uid", nsmgr).InnerText;
            string squarePictureUrl = friendElement.SelectSingleNode("fb:pic_square", nsmgr).InnerText;

            return new Friend(firstName, lastName, uid, squarePictureUrl);
        }

        /// <summary>
        /// Serializes an AccountLink object into a XmlElement.
        /// </summary>
        /// <param name="accountLink">
        /// The AccountLink to serialize.
        /// </param>
        /// <param name="document">
        /// A XmlDocument to serialize the AccountLink into.
        /// </param>
        /// <returns>
        /// A XmlElement representing an AccountLink.
        /// </returns>
        public static XmlElement SerializeAccountLink(AccountLink accountLink, XmlDocument document)
        {
            if (document == null)
            {
                throw new ArgumentNullException("document");
            }
            if (accountLink == null)
            {
                throw new ArgumentNullException("accountLink");
            }

            XmlElement accountLinkElement = document.CreateElement("account_link");

            XmlElement personTagElement = document.CreateElement("person_tag");
            personTagElement.InnerText = accountLink.PersonTag.Name;
            accountLinkElement.AppendChild(personTagElement);

            XmlElement facebookFriendElement = SerializeFriend(accountLink.FacebookFriend, document);
            accountLinkElement.AppendChild(facebookFriendElement);

            return accountLinkElement;
        }

        /// <summary>
        /// Deserializes a XmlElement into an AccountLink.
        /// </summary>
        /// <param name="accountLinkElement">
        /// A XmlElement representing an AccountLink.
        /// </param>
        /// <returns>
        /// An AccountLink.
        /// </returns>
        public static AccountLink DeserializeAccountLink(XmlElement accountLinkElement)
        {
            if (accountLinkElement == null)
            {
                throw new ArgumentNullException("accountLinkElement");
            }

            string personTagName = accountLinkElement.SelectSingleNode("person_tag").InnerText;
            PersonTag personTag = new PersonTag(personTagName);

            XmlElement facebookFriendElement = 
                accountLinkElement.SelectSingleNode("user") as XmlElement;
            Friend facebookFriend = DeserializeFriend(facebookFriendElement);

            return new AccountLink(personTag, facebookFriend);
        }

        /// <summary>
        /// Deserializes a XmlElement into a Permissions object.
        /// </summary>
        /// <param name="accountLinkElement">
        /// A XmlElement representing a Permissions object.
        /// </param>
        /// <returns>
        /// A Permissions object.
        /// </returns>
        public static Permissions DeserializePermissions(XmlElement permissionElement)
        {
            if (permissionElement == null)
            {
                throw new ArgumentNullException("permissionElement");
            }

            XmlNamespaceManager nsmgr = GetFacebookNamespaceManager(permissionElement);

            string offlineAccessText = permissionElement.SelectSingleNode("fb:offline_access", nsmgr).InnerText.Trim();
            string photoUploadText = permissionElement.SelectSingleNode("fb:photo_upload", nsmgr).InnerText.Trim();
            string videoUploadText = permissionElement.SelectSingleNode("fb:video_upload", nsmgr).InnerText.Trim();

            bool offlineAccess, photoUpload, videoUpload;
            offlineAccess = offlineAccessText == "1" ? true : false;
            photoUpload = photoUploadText == "1" ? true : false;
            videoUpload = videoUploadText == "1" ? true : false;

            return new Permissions(offlineAccess, photoUpload, videoUpload);
        }

        #endregion
    }
}